package mspbots.teams.client.oauth;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.log4j.Log4j2;
import mspbots.teams.common.RedisKeys;
import mspbots.teams.common.annotation.ApiException;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.time.Duration;

/**
 * com.mspbots.doctor.core.AuthService
 *
 * @author Alex bob(https://github.com/vnobo)
 * @date Created by 2020/3/19
 */
@Log4j2
@Service
public class AuthService {

    private final static String MICROSOFT_ONLINE_AUTHORITY_URL = "https://login.microsoftonline.com/common";

    private final AuthProperties authProperties;
    private final WebClient webClient;
    private final ReactiveRedisTemplate<String, Object> reactiveRedisTemplate;
    private final ObjectMapper objectMapper;

    public AuthService(AuthProperties authProperties, WebClient.Builder builder,
                       ReactiveRedisTemplate<String, Object> reactiveRedisTemplate,
                       ObjectMapper objectMapper) {
        this.authProperties = authProperties;
        this.webClient = builder.baseUrl(MICROSOFT_ONLINE_AUTHORITY_URL).build();
        this.reactiveRedisTemplate = reactiveRedisTemplate;
        this.objectMapper = objectMapper;
    }

    public Mono<String> authToken(TokenState state, String code) {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>(10);
        params.set("grant_type", "authorization_code");
        params.set("redirect_uri", this.authProperties.getRedirectUri());
        if ("user".equals(state.getTokenType().name())) {
            params.set("scope", this.authProperties.getScope());
        } else {
            params.set("scope", this.authProperties.getScope() + " User.Read.All Group.Read.All");
        }
        params.set("code", code);

        return this.tokenClientGet(state, params)
                .map(TokenState::getAccessToken)
                .doOnNext(auth -> this.reactiveRedisTemplate.opsForValue()
                        .delete(RedisKeys.USERS_ANCHOR_REDIS_KEY_PREFIX + state.getTenantId())
                        .subscribe());
    }

    public Mono<TokenState> getToken(TokenState state) {

        Mono<TokenState> accessTokenMono = this.reactiveRedisTemplate.opsForValue()
                .get(RedisKeys.ACCESS_TOKEN_TENANT_REDIS_KEY_PREFIX + state.getTenantId())
                .map(result -> this.objectMapper.convertValue(result, TokenState.class));

        Mono<AuthorizationCode> authorizationCodeMono = this.reactiveRedisTemplate.opsForValue()
                .get(RedisKeys.OAUTH_TENANT_REDIS_KEY_PREFIX + state.getTenantId())
                .map(result -> this.objectMapper.convertValue(result, AuthorizationCode.class));

        return accessTokenMono
                .switchIfEmpty(authorizationCodeMono.flatMap(authorizationCode ->
                        this.refreshToken(state, authorizationCode.getRefreshToken())
                                .map(authCode -> state.setAccessToken(authCode.getAccessToken()))))
                .switchIfEmpty(Mono.defer(() -> {
                    if (state.getTokenType() == TokenState.TokenType.loginToken) {
                        return Mono.just(state);
                    }
                    return Mono.error(new TeamsNoAuthorizationException(
                            this.objectMapper.createObjectNode().put("error", "This tenant ["
                                    + state.getTenantId() + "] " +
                                    "access token is empty, Allow access your account.")));
                }));

    }

    private Mono<TokenState> refreshToken(TokenState state, String refreshToken) {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>(10);
        params.set("grant_type", "refresh_token");
        params.set("refresh_token", refreshToken);
        return this.tokenClientGet(state, params);
    }


    private Mono<TokenState> tokenClientGet(TokenState state, MultiValueMap<String, String> params) {
        params.set("client_id", this.authProperties.getClientId());
        params.set("client_secret", this.authProperties.getSecretKey());
        log.debug("Post token params:{}", params);
        return this.webClient.post().uri(uriBuilder -> uriBuilder.path("/oauth2/v2.0/token").build())
                .accept(MediaType.APPLICATION_FORM_URLENCODED)
                .bodyValue(params)
                .retrieve()
                .onStatus(HttpStatus::isError, clientResponse -> clientResponse.bodyToMono(JsonNode.class)
                        .map(errMsg -> new ApiException(errMsg, clientResponse.statusCode())))
                .bodyToMono(JsonNode.class)
                .map(jsonNode -> AuthorizationCode.builder().accessToken(jsonNode.get("access_token").asText())
                        .scope(jsonNode.get("scope").asText())
                        .refreshToken(jsonNode.get("refresh_token").asText())
                        .expiresIn(jsonNode.get("expires_in").asLong())
                        .tokenType(jsonNode.get("token_type").asText())
                        .build())
                .flatMap(authCode -> {
                    log.debug("Return auth code :{}", authCode);
                    authCode.setTenantId(state.getTenantId());
                    state.setAccessToken(authCode.getAccessToken());
                    return this.reactiveRedisTemplate.opsForValue()
                            .set(RedisKeys.OAUTH_TENANT_REDIS_KEY_PREFIX + authCode.getTenantId(), authCode,
                                    Duration.ofDays(90))
                            .flatMap(res -> this.reactiveRedisTemplate.opsForValue()
                                    .set(RedisKeys.ACCESS_TOKEN_TENANT_REDIS_KEY_PREFIX + state.getTenantId(),
                                            state, Duration.ofSeconds(authCode.getExpiresIn() - 300)))
                            .map(res -> state);
                });
    }

    private static class TeamsNoAuthorizationException extends ApiException {

        public TeamsNoAuthorizationException(JsonNode message) {
            super(message, HttpStatus.NOT_FOUND);
        }
    }

}
